# -*- coding: utf-8 -*-
import os
from time import time, asctime, gmtime
from datetime import datetime, timedelta
import hashlib
try:
    import cPickle as pickle
except ImportError:
    import pickle


class BaseCache(object):
    def __init__(self, timeout=300):
        try:
            timeout = int(timeout)
        except (ValueError, TypeError):
            timeout = 300
        self.default_timeout = timeout

    def add(self, key, value, timeout=None):
        """
        Set a value in the cache if the key does not already exist. If
        timeout is given, that timeout will be used for the key; otherwise
        the default cache timeout will be used.

        Returns True if the value was stored, False otherwise.
        """
        raise NotImplementedError

    def get(self, key, default=None):
        """
        Fetch a given key from the cache. If the key does not exist, return
        default, which itself defaults to None.
        """
        raise NotImplementedError

    def set(self, key, value, timeout=None):
        """
        Set a value in the cache. If timeout is given, that timeout will be
        used for the key; otherwise the default cache timeout will be used.
        """
        raise NotImplementedError

    def delete(self, key):
        """
        Delete a key from the cache, failing silently.
        """
        raise NotImplementedError

    def get_many(self, keys):
        """
        Fetch a bunch of keys from the cache. For certain backends (memcached,
        pgsql) this can be *much* faster when fetching multiple values.

        Returns a dict mapping each key in keys to its value. If the given
        key is missing, it will be missing from the response dict.
        """
        d = {}
        for k in keys:
            val = self.get(k)
            if val is not None:
                d[k] = val
        return d

    def has_key(self, key):
        """
        Returns True if the key is in the cache and has not expired.
        """
        return self.get(key) is not None

    def incr(self, key, delta=1):
        """
        Add delta to value in the cache. If the key does not exist, raise a
        ValueError exception.
        """
        if key not in self:
            raise ValueError, "Key '%s' not found" % key
        new_value = self.get(key) + delta
        self.set(key, new_value)
        return new_value

    def decr(self, key, delta=1):
        """
        Subtract delta from value in the cache. If the key does not exist, raise
        a ValueError exception.
        """
        return self.incr(key, -delta)

    def __contains__(self, key):
        """
        Returns True if the key is in the cache and has not expired.
        """
        # This is a separate method, rather than just a copy of has_key(),
        # so that it always has the same functionality as has_key(), even
        # if a subclass overrides it.
        return self.has_key(key)

    def cache_page(self, timeout=None, prefix="accost.page_cache."):
        if timeout is None:
            timeout = self.default_timeout
        def wrap(get_func):
            def new_get(handler):
                key = prefix + hashlib.md5(handler.request.url).hexdigest()
                resp = self.get(key)
                if resp is None:
                    response = get_func(handler)
                    if response.status_code != 200:
                        return response

                    now = datetime.now()
                    if not "last-modified" in response.headers:
                        response.last_modified = now
                    expiry = asctime(gmtime(time() + timeout))
                    if not "expires" in response.headers:
                        response.expires = now + timedelta(seconds=timeout)
                    if not "cache-control" in response.headers:
                        response.cache_control.max_age = timeout
                    if response.get_etag() == (None, None):
                        response.set_etag(hashlib.md5(response.data).hexdigest())

                    response.freeze()
                    self.set(key, response, timeout)
                    return response
                else:
                    return resp

            return new_get

        return wrap


class DummyCache(BaseCache):
    def __init__(self, *args, **kwargs):
        pass

    def add(self, *args, **kwargs):
        return True

    def get(self, key, default=None):
        return default

    def set(self, *args, **kwargs):
        pass

    def delete(self, *args, **kwargs):
        pass

    def get_many(self, *args, **kwargs):
        return {}

    def has_key(self, *args, **kwargs):
        return False


def to_str(s, encoding="utf-8", force=True):
    if isinstance(s, unicode):
        return s.encode(encoding)
    return str(s) if force else s


def to_unicode(s, encoding="utf-8", force=True):
    if isinstance(s, str):
        return s.decode(encoding)
    return unicode(s) if force else s


class MemcacheCache(BaseCache):
    def __init__(self, servers, timeout=300):
        BaseCache.__init__(self, timeout)
        try:
            import cmemecache as memcache
        except ImportError:
            import memcache
        self._cache = memcache.Client(servers)

    def add(self, key, value, timeout=0):
        return self._cache.add(to_str(key), value, timeout or self.default_timeout)

    def get(self, key, default=None):
        val = self._cache.get(to_str(key))
        if val is None:
            return default
        else:
            return val

    def set(self, key, value, timeout=0):
        self._cache.set(to_str(key), value, timeout or self.default_timeout)

    def delete(self, key):
        self._cache.delete(to_str(key))

    def get_many(self, keys):
        return self._cache.get_multi(map(to_str, keys))

    def close(self, **kwargs):
        self._cache.disconnect_all()

    def incr(self, key, delta=1):
        return self._cache.incr(key, delta)

    def decr(self, key, delta=1):
        return self._cache.decr(key, delta)


class GAECache(MemcacheCache):
    def __init__(self, timeout=300):
        BaseCache.__init__(self, timeout)
        from google.appengine.api import memcache
        self._cache = memcache

class FileCache(BaseCache):
    def __init__(self, dir, timeout=300, max_entries=300, cull_frequency=3):
        BaseCache.__init__(self, timeout)

        try:
            self._max_entries = int(max_entries)
        except (ValueError, TypeError):
            self._max_entries = 300

        try:
            self._cull_frequency = int(cull_frequency)
        except (ValueError, TypeError):
            self._cull_frequency = 3

        self._dir = dir
        if not os.path.exists(self._dir):
            self._createdir()

    def add(self, key, value, timeout=None):
        if self.has_key(key):
            return False

        self.set(key, value, timeout)
        return True

    def get(self, key, default=None):
        fname = self._key_to_file(key)
        try:
            f = open(fname, 'rb')
            exp = pickle.load(f)
            now = time()
            if exp < now:
                f.close()
                self._delete(fname)
            else:
                return pickle.load(f)
        except (IOError, OSError, EOFError, pickle.PickleError):
            pass
        return default

    def set(self, key, value, timeout=None):
        fname = self._key_to_file(key)
        dirname = os.path.dirname(fname)

        if timeout is None:
            timeout = self.default_timeout

        self._cull()

        try:
            if not os.path.exists(dirname):
                os.makedirs(dirname)

            f = open(fname, 'wb')
            now = time()
            pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL)
            pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
        except (IOError, OSError):
            pass

    def delete(self, key):
        try:
            self._delete(self._key_to_file(key))
        except (IOError, OSError):
            pass

    def _delete(self, fname):
        os.remove(fname)
        try:
            # Remove the 2 subdirs if they're empty
            dirname = os.path.dirname(fname)
            os.rmdir(dirname)
            os.rmdir(os.path.dirname(dirname))
        except (IOError, OSError):
            pass

    def has_key(self, key):
        fname = self._key_to_file(key)
        try:
            f = open(fname, 'rb')
            exp = pickle.load(f)
            now = time()
            if exp < now:
                f.close()
                self._delete(fname)
                return False
            else:
                return True
        except (IOError, OSError, EOFError, pickle.PickleError):
            return False

    def _cull(self):
        if int(self._num_entries) < self._max_entries:
            return

        try:
            filelist = os.listdir(self._dir)
        except (IOError, OSError):
            return

        if self._cull_frequency == 0:
            doomed = filelist
        else:
            doomed = [os.path.join(self._dir, k) for (i, k) in enumerate(filelist) if i % self._cull_frequency == 0]

        for topdir in doomed:
            try:
                for root, _, files in os.walk(topdir):
                    for f in files:
                        self._delete(os.path.join(root, f))
            except (IOError, OSError):
                pass

    def _createdir(self):
        try:
            os.makedirs(self._dir)
        except OSError:
            raise EnvironmentError, "Cache directory '%s' does not exist and could not be created'" % self._dir

    def _key_to_file(self, key):
        """
        Convert the filename into an md5 string. We'll turn the first couple
        bits of the path into directory prefixes to be nice to filesystems
        that have problems with large numbers of files in a directory.

        Thus, a cache key of "foo" gets turnned into a file named
        ``{cache-dir}ac/bd/18db4cc2f85cedef654fccc4a4d8``.
        """
        path = hashlib.md5(key.encode('utf-8')).hexdigest()
        path = os.path.join(path[:2], path[2:4], path[4:])
        return os.path.join(self._dir, path)

    def _get_num_entries(self):
        count = 0
        for _,_,files in os.walk(self._dir):
            count += len(files)
        return count
    _num_entries = property(_get_num_entries)
